Hash files are cached

Pitfall By design
Hash files render once and are cached, so per-request expressions like #docly.foo(request.Jwt.x)# freeze the first visitor's data and serve it to everyone. Dynamic per-user data must come from #/API/-endpoints fetched by browser JS at runtime.

What you'll see

A developer — or more often an AI code-generation tool — writes a hash file (.hash) that embeds per-request data inline using #...#-expressions. Common shapes:

<img src="#docly.getProfilePictureUrl(request.Jwt.username)#" alt="Profile">
<p>Welcome, #request.Jwt.username#</p>
#if (!request.Jwt) docly.denyAccess();#

The first request renders correctly. From that point on, every visitor sees the first user’s cached HTML — including their username, profile picture, or (worst) a denyAccess() call that fired for a logged-out user and now blocks logged-in users.

What's actually happening

Docly hash files are display templates that the HashJS engine renders to HTML and caches. The cache is keyed by the file path, not by request — #...#-expressions are evaluated once when the cache is built, then the cached output is served to all subsequent visitors without re-evaluation.

This is intentional. Hash rendering is what makes Docly-served pages fast: a hash file with no per-request expressions can be served straight from cache with no JS evaluation at all. The tradeoff is that anything depending on the current request — request.Jwt, request.IP, request.UserAgent, query parameters, the current time, the currently logged-in user — must not appear inside #...# in a hash file.

The vulnerability class this creates is data leakage: the first user to hit a page after cache invalidation populates the cache with their own session data, and every subsequent user receives that data until the cache is rebuilt. For a profile-picture URL this is a privacy bug; for an admin-only block (docly.denyAccess()) it can become a security incident.

What to do

Put per-request logic in #/API/-functions. From the hash file, use static HTML plus browser JS that calls the API at runtime via fetch(). The hash itself stays static and cacheable; the dynamic data flows through the API on every request.

Do — split static hash from dynamic API:

// #/API/profile.js
export default () => {
    if (!request.Jwt) return docly.denyAccess();
    return {
        username: request.Jwt.username,
        pictureUrl: docly.getProfilePictureUrl(request.Jwt.username)
    };
}
<!-- profile.hash — static HTML, dynamic data fetched at runtime -->
<img id="avatar" alt="Profile">
<p>Welcome, <span id="name"></span></p>
<script>
    fetch('/API/profile')
        .then(r => r.json())
        .then(d => {
            document.getElementById('avatar').src = d.pictureUrl;
            document.getElementById('name').innerText = d.username;
        });
</script>

Don’t — per-request expression in hash:

<img src="#docly.getProfilePictureUrl(request.Jwt.username)#" alt="Profile">
<p>Welcome, #request.Jwt.username#</p>

Access control follows the same rule: never put docly.denyAccess() or request.Jwt-checks inside #...# in a hash file. Protect the endpoint behind the data instead — let the API call denyAccess(), and let the hash page show a “loading…” state until the fetch resolves or fails.